<?php

declare(strict_types=1);

namespace App\Command\Gen;

use Hyperf\Command\Command as HyperfCommand;
use Hyperf\Command\Annotation\Command;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Database\ConnectionResolverInterface;
use Hyperf\Database\Schema\Builder;
use Hyperf\Stringable\Str;
use Hyperf\CodeParser\Project;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

#[Command]
class ServicesCommand extends HyperfCommand
{
    protected ?ConnectionResolverInterface $resolver = null;

    protected ?ConfigInterface $config = null;

    public function __construct(protected ContainerInterface $container)
    {
        parent::__construct('gen:services');
        $this->setDescription('Create new Services classes.');
    }

    public function run(InputInterface $input, OutputInterface $output): int
    {
        $this->resolver = $this->container->get(ConnectionResolverInterface::class);

        $this->config = $this->container->get(ConfigInterface::class);

        return parent::run($input, $output);
    }

    public function configure()
    {
        parent::configure();
        $this->addArgument('table', InputArgument::OPTIONAL, 'Which table you want to associated with the Model.');

        $this->addOption('path', null, InputOption::VALUE_OPTIONAL, 'The path that you want the Services file to be generated.');
        $this->addOption('uses', 'U', InputOption::VALUE_OPTIONAL, 'The default class uses of the Services.');
        $this->addOption('inheritance', 'i', InputOption::VALUE_OPTIONAL, 'The inheritance that you want the Services extends.');
        $this->addOption('prefix', 'P', InputOption::VALUE_OPTIONAL, 'What prefix that you want the Services set.');
        $this->addOption('pool', 'p', InputOption::VALUE_OPTIONAL, 'Which connection pool you want the Services use.', 'default');
        $this->addOption('ignore-tables', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Ignore tables for creating models.');
    }

    public function handle()
    {
        $table = $this->input->getArgument('table');
        $pool = $this->input->getOption('pool');
        $option = new ServicesOption();
        $option->setPool($pool)
            ->setPath($this->getOption('path', 'commands.gen:services.path', $pool, 'app/Services'))
            ->setUses($this->getOption('uses', 'commands.gen:services.uses', $pool, 'App\Utils\Services\BaseServices'))
            ->setInheritance($this->getOption('inheritance', 'commands.gen:services.inheritance', $pool, 'BaseServices'))
            ->setPrefix($this->getOption('prefix', 'prefix', $pool, ''))
            ->setIgnoreTables($this->getOption('ignore-tables','commands.gen:services.ignore_tables', $pool, []));

        if ($table) {
            $this->createService($table, $option);
        } else {
            $this->createServices($option);
        }
    }

    protected function createServices(ServicesOption $option)
    {
        $builder = $this->getSchemaBuilder($option->getPool());
        $tables = [];

        foreach ($builder->getAllTables() as $row) {
            $row = (array) $row;
            $table = reset($row);
            if (! $this->isIgnoreTable($table, $option)) {
                $tables[] = $table;
            }
        }

        foreach ($tables as $table) {
            $this->createService($table, $option);
        }
    }

    protected function isIgnoreTable(string $table, ServicesOption $option): bool
    {
        if (in_array($table, $option->getIgnoreTables())) {
            return true;
        }
        return $table === $this->config->get('databases.migrations', 'migrations');
    }

    protected function getSchemaBuilder(string $poolName): Builder
    {
        $connection = $this->resolver->connection($poolName);
        return $connection->getSchemaBuilder();
    }

    protected function createService(?string $table, ServicesOption $option)
    {
        $table = Str::replaceFirst($option->getPrefix(), '', $table);
        $project = new Project();
        $class = $option->getTableMapping()[$table] ?? Str::studly(Str::singular($table));
        $class = $project->namespace($option->getPath()) . $class;
        $path = BASE_PATH . '/' . $project->path($class);

        if (! file_exists($path)) {
            $this->mkdir($path);
            file_put_contents($path, $this->buildClass($table, $class, $option));
        }

        $this->output->writeln(sprintf('<info>%s</info>', $class . ' created successfully.'));
    }

    /**
     * Build the class with the given name.
     */
    protected function buildClass(string $table, string $name, ServicesOption $option): string
    {
        $stub = file_get_contents(BASE_PATH . '/app/Command/Gen/stubs/Services.stub');

        $model = str_replace(['\Services'], ['\Model'], $name);

        return $this->replaceNamespace($stub, $name)
            ->replaceInheritance($stub, $option->getInheritance())
            ->replaceUses($stub, $option->getUses())
            ->replaceClass($stub, $name)
            ->replaceModel($stub, $model, str_replace($this->getNamespace($name) . '\\', '', $name))
            ->replaceTable($stub, $table);
    }

    protected function replaceInheritance(string &$stub, string $inheritance): self
    {
        $stub = str_replace(
            ['%INHERITANCE%'],
            [$inheritance],
            $stub
        );

        return $this;
    }

    protected function replaceUses(string &$stub, string $uses): self
    {
        $uses = $uses ? "use {$uses};" : '';
        $stub = str_replace(
            ['%USES%'],
            [$uses],
            $stub
        );

        return $this;
    }

    protected function replaceModel(string &$stub, string $model, string $modelName): self
    {
        $model = $model ? "use {$model} as {$modelName}Model;" : '';
        $stub = str_replace(
            ['%USEMODEL%'],
            [$model],
            $stub
        );

        return $this;
    }

    /**
     * Replace the namespace for the given stub.
     */
    protected function replaceNamespace(string &$stub, string $name): self
    {
        $stub = str_replace(
            ['%NAMESPACE%'],
            [$this->getNamespace($name)],
            $stub
        );

        return $this;
    }

    /**
     * Get the full namespace for a given class, without the class name.
     */
    protected function getNamespace(string $name): string
    {
        return trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\');
    }

    /**
     * Replace the class name for the given stub.
     */
    protected function replaceClass(string &$stub, string $name): self
    {
        $class = str_replace($this->getNamespace($name) . '\\', '', $name);

        $stub = str_replace('%CLASS%', $class, $stub);

        return $this;
    }

    /**
     * Replace the table name for the given stub.
     */
    protected function replaceTable(string $stub, string $table): string
    {
        return str_replace('%TABLE%', $table, $stub);
    }

    protected function getOption(string $name, string $key, string $pool = 'default', $default = null)
    {
        $result = $this->input->getOption($name);
        $nonInput = null;
        if (in_array($name, ['force-casts', 'refresh-fillable', 'with-comments', 'with-ide'])) {
            $nonInput = false;
        }
        if (in_array($name, ['table-mapping', 'ignore-tables', 'visitors'])) {
            $nonInput = [];
        }

        if ($result === $nonInput) {
            $result = $this->config->get("databases.{$pool}.{$key}", $default);
        }
        return $result;
    }

    protected function mkdir(string $path): void
    {
        $dir = dirname($path);
        if (! is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }
    }
}
